/**
 * Adelson-Velsky and Landis Tree
 * [Wikipedia](https://en.wikipedia.org/wiki/AVL_tree)
 * [A video lecture](http://www.youtube.com/watch?v=TbvhGcf6UJU)
 */
'use strict'

/**
 * A utility class for comparator
 * A comparator is expected to have following structure
 *
 * comp(a, b) RETURN < 0 if a < b
 * RETURN > 0 if a > b
 * MUST RETURN 0 if a == b
 */
let utils;
(function (_utils) {
  function comparator () {
    return function (v1, v2) {
      if (v1 < v2) {
        return -1
      } else if (v2 < v1) {
        return 1
      } else {
        return 0
      }
    }
  }
  _utils.comparator = comparator
})(utils || (utils = {}))

/**
 * @constructor
 * A class for AVL Tree
 * @argument comp - A function used by AVL Tree For Comparison
 * If no argument is sent it uses utils.comparator
 */
const AVLTree = (function () {
  function _avl (comp) {
    /** @public comparator function */
    this._comp = undefined
    if (comp !== undefined) {
      this._comp = comp
    } else {
      this._comp = utils.comparator()
    }
    /** @public root of the AVL Tree */
    this.root = null
    /** @public number of elements in AVL Tree */
    this.size = 0
  }
  // creates new Node Object
  const Node = function (val) {
    this._val = val
    this._left = null
    this._right = null
    this._height = 1
  }
  // get height of a node
  const getH = function (node) {
    if (node == null) { return 0 }
    return node._height
  }
  // height difference or balance factor of a node
  const getHDiff = function (node) {
    if (node == null) { return 0 } else { return getH(node._left) - getH(node._right) }
  }
  // update height of a node based on children's heights
  const updateH = function (node) {
    if (node == null) {
      return
    }
    node._height = Math.max(getH(node._left), getH(node._right)) + 1
  }
  // rotations of AVL Tree
  const leftRotate = function (node) {
    const temp = node._right
    node._right = temp._left
    temp._left = node
    updateH(node)
    updateH(temp)
    return temp
  }
  const rightRotate = function (node) {
    const temp = node._left
    node._left = temp._right
    temp._right = node
    updateH(node)
    updateH(temp)
    return temp
  }
  // check if tree is balanced else balance it for insertion
  const insertBalance = function (node, _val, balanceFactor) {
    if (balanceFactor > 1 && _val < node._left._val) {
      return rightRotate(node) // Left Left Case
    } else if (balanceFactor < 1 && _val > node._right._val) {
      return leftRotate(node) // Right Right Case
    } else if (balanceFactor > 1 && _val > node._left._val) {
      node._left = leftRotate(node._left) // Left Right Case
      return rightRotate(node)
    }
    node._right = rightRotate(node._right)
    return leftRotate(node)
  }
  // check if tree is balanced after deletion
  const delBalance = function (node) {
    const balanceFactor1 = getHDiff(node)
    if (balanceFactor1 === 0 || balanceFactor1 === 1 || balanceFactor1 === -1) {
      return node
    }
    if (balanceFactor1 > 1) {
      if (getHDiff(node._left) >= 0) {
        return rightRotate(node) // Left Left
      }
      node._left = leftRotate(node._left)
      return rightRotate(node) // Left Right
    }
    if (getHDiff(node._right) > 0) {
      node._right = rightRotate(node._right)
      return leftRotate(node) // Right Left
    }
    return leftRotate(node) // Right Right
  }
  // implement avl tree insertion
  const insert = function (root, val, tree) {
    if (root == null) {
      tree.size++
      return new Node(val)
    } else if (tree._comp(root._val, val) < 0) {
      root._right = insert(root._right, val, tree)
    } else if (tree._comp(root._val, val) > 0) {
      root._left = insert(root._left, val, tree)
    } else {
      return root
    }
    updateH(root)
    const balanceFactor = getHDiff(root)
    if (balanceFactor === 0 || balanceFactor === 1 || balanceFactor === -1) {
      return root
    }
    return insertBalance(root, val, balanceFactor)
  }
  // delete a element
  const del = function (root, _val, tree) {
    if (root == null) {
      return root
    } else if (tree._comp(root._val, _val) === 0) { // key found case
      if (root._left === null && root._right === null) {
        root = null
        tree.size--
      } else if (root._left === null) {
        root = root._right
        tree.size--
      } else if (root._right === null) {
        root = root._left
        tree.size--
      } else {
        let temp = root._right
        while (temp._left != null) {
          temp = temp._left
        }
        root._val = temp._val
        root._right = del(root._right, temp._val, tree)
      }
    } else {
      if (tree._comp(root._val, _val) < 0) {
        root._right = del(root._right, _val, tree)
      } else {
        root._left = del(root._left, _val, tree)
      }
    }
    updateH(root)
    root = delBalance(root)
    return root
  }
  // search tree for a element
  const search = function (root, val, tree) {
    if (root == null) {
      return null
    } else if (tree._comp(root._val, val) === 0) {
      return root
    } else if (tree._comp(root._val, val) < 0) {
      return search(root._right, val, tree)
    }
    return search(root._left, val, tree)
  }

  /* Public Functions */
  /**
   * For Adding Elements to AVL Tree
   * @param {any} _val
   * Since in AVL Tree an element can only occur once so
   * if a element exists it return false
   * @returns {Boolean} element added or not
   */
  _avl.prototype.add = function (_val) {
    const prevSize = this.size
    this.root = insert(this.root, _val, this)
    if (this.size === prevSize) {
      return false
    }
    return true
  }
  /**
   * TO check is a particular element exists or not
   * @param {any} _val
   * @returns {Boolean} exists or not
   */
  _avl.prototype.find = function (_val) {
    const temp = search(this.root, _val, this)
    if (temp != null) {
      return true
    }
    return false
  }
  /**
   *
   * @param {any} _val
   * It is possible that element doesn't exists in tree
   * in that case it return false
   * @returns {Boolean} if element was found and deleted
   */
  _avl.prototype.remove = function (_val) {
    const prevSize = this.size
    this.root = del(this.root, _val, this)
    if (prevSize === this.size) {
      return false
    }
    return true
  }
  return _avl
}())

/**
 * A Code for Testing the AVLTree
 */
// (function test () {
//   const newAVL = new AVLTree()
//   const size = Math.floor(Math.random() * 1000000)
//   let uniques = 0
//   let i, temp, j
//   const array = []
//   for (i = 0; i < size; i++) {
//     temp = Math.floor(Math.random() * Number.MAX_VALUE)
//     if (newAVL.add(temp)) {
//       uniques++
//       array.push(temp)
//     }
//   }
//   if (newAVL.size !== uniques) {
//     throw new Error('elements not inserted properly')
//   }
//   const findTestSize = Math.floor(Math.random() * uniques)
//   for (i = 0; i < findTestSize; i++) {
//     j = Math.floor(Math.random() * uniques)
//     if (!newAVL.find(array[j])) {
//       throw new Error('inserted elements not found')
//     }
//   }
//   const deleteTestSize = Math.floor(uniques * Math.random())
//   for (i = 0; i < deleteTestSize; i++) {
//     j = Math.floor(Math.random() * uniques)
//     temp = array[j]
//     if (newAVL.find(temp)) {
//       if (!newAVL.remove(temp)) {
//         throw new Error('delete not working properly')
//       }
//     }
//   }
// })()

export { AVLTree }
